<?php
/**
 * Created by PhpStorm.
 * User: lihui
 * Date: 3/26/2021
 * Time: 16:36
 */
namespace app\common\model\coin;

use app\command\HistoryTrade;
use app\command\UdpServer;
use mall\library\Huobi;
use think\facade\Db;

/**
 * 调用API操作
 * Class CallApi
 * @package app\common\model\coin
 */
class CallApi extends CoinRuleRunTrade
{

    //第三方接口类型
    const METHOD_PLACE_ORDER        = 'place_order';  // 火币下单接口
    const METHOD_GET_CLIENT_ORDER   = 'get_client_order';  // 通过自编ID获取订单详情
    const METHOD_GET_ORDER          = 'get_order';  // 通过订单ID获取订单详情
    const METHOD_GET_ORDER_MATCHRESULTS          = 'get_order_matchresults';  // 通过订单ID，获取交易详情

    //错误代码对应的类型
    const CALL_ERROR_CODE_TYPES = [
        -1    => 'NETWORK', //网络问题，本地
        'base-system-error' => 'SYSTEM_ERROR',  //第三方系统错误
        'base-record-invalid'   => 'REOCRD_INVALID',    //数据无效
        'account-get-accounts-inexistent-error' => 'ACCOUNT_NOT_EXIST',  // 账号问题
        'account-account-id-inexistent' => 'ACCOUNT_NOT_EXIST', //账户不存在
        'base-symbol-trade-disabled'    => 'SYMBOL_DISABLED',    //交易对禁止交易
        'base-operation-forbidden'    => 'SYMBOL_DISABLED_FOR_USER',    //用户禁止交易该币种
        'order-accountbalance-error'    => 'BALANCE_ERROR',     //余额不足
        'account-frozen-balance-insufficient-error'    => 'BALANCE_ERROR',     //余额不足【冻结金额不足】
        'order-orderprice-precision-error'  => 'PRICE_PRECISION',   //价格精准度问题
        'order-orderamount-precision-error' => 'AMOUNT_PRECISION',  //交易数额精准度问题
        'invalid-client-order-id' => 'CLIENT_ID_ERROR',  // 客户ID错误，客户ID重复
        'DEFAULT'   => 'UNKNOWN',   // 未定义的错误类型【服务器】
    ];

    /**
     * 火币订单状态转为自定义状态
     */
    const CALL_ORDER_STATE_LIST = [
        'created'       => 'CREATE',    //创建    1
        'submitted'    => 'MATCH', //撮合中    2
        'partial-filled'    => 'PARTIAL_FILLED', //部分成交 3
        'canceling'     => 'CANCELING', //撤销中   4
        'filled'       => 'FILLED',    //订单已全部撮合完成  5
        'partial-canceled'  => 'PARTIAL_CANCELED',  //部分撤销  6
        'canceled'     => 'CANCELED',  //全部撤销   7
        'DEFAULT'   => 'UNKNOWN',   // 未定义的错误类型
    ];

    // 判断是否为测试
    const ENV_CHECK_TEST_KEY = 'call_api.is_test';



    /**
     * ENV 配置参数KEY列表
     */
    const ENV_KEY_LIST = [
        'SYNC_REQUEST_ORDER'    => 'call_api.sync_request_order', //同步请求api
        'SYNC_REQUEST_TIMEOUT'  => 'call_api.sync_request_timeout', // 同步请求超时时间
    ];

    /**
     * 第三方交易类型对应本地类型
     */
    const API_ORDER_TYPE_TO_TYPE = [
        'buy-market'    => 'BUY_MARKET',    //市价买入
        'sell-limit'    => 'SELL_LIMIT',    //市价限价卖出
        'sell-market'   => 'SELL_MARKET',   //市价卖
    ];


    /**
     * 判断买卖类型
     * @param $type 卖出还是买入
     * @param null $extType 各自对应的类型【目前卖出】
     * @param null $extPrice    实际价格判断
     * @return string
     */
    private function getPlaceOrderType($type,$extType = null,$extPrice = null) {
        switch($type) {
            case static::TYPE_BUY:
                return 'buy-market';
            case static::TYPE_SELL:
                $return = 'sell-limit';
                if(!is_null($extType) && $extType !== '') {  // 按类型判断
                    switch($extType) {
                        case CoinRule::SELL_TYPE_LIST['MARKET']: //市价卖
                            $return = 'sell-market';
                            break;
                    }
                } else if(!is_null($extPrice) && $extPrice !== '') {
                    if($extPrice == 0) {
                        $return = 'sell-market';
                    }
                }
                return $return;
        }
    }


    /**
     * 下单操作
     * @param array $tradeDetails 添加的详情
     * @param array $userKey    用户密钥信息 必须存在字段：account_id、access_key、secret_key
     * @param boolean $returnErrType 是否返回错误字符串
     * @param string $symbol   操作币种
     */
    public function placeOrder(array $tradeDetails,array $userKey,string $symbol,bool $returnErrType = null) {
        $isSell = false;
        $typeToApiType = array_flip(self::API_ORDER_TYPE_TO_TYPE);
        $price = 0;
        $apiSellType = $this->getPlaceOrderType($tradeDetails['type'],null,$tradeDetails['unit_price']);
        switch ($apiSellType) {
            case $typeToApiType['BUY_MARKET']:  //市价买
                $amount = bcsub($tradeDetails['amount'],0,$userKey['buy_money_scale']);
                break;
            case $typeToApiType['SELL_LIMIT']:  // 限价卖
                $price = bcsub($tradeDetails['unit_price'],0,$userKey['sell_price_scale']);
            case $typeToApiType['SELL_MARKET']: //市价卖
                $isSell = true; //卖出
                $amount = bcsub($tradeDetails['quantity'],0,$userKey['sell_number_scale']);
                break;
            default:
                return $returnErrType?static::getErrorType('DEFAULT',''):false;
        }

        // 判断是否需要请求接口
        if(bccomp($amount,0,$this->scale) == 0 && bccomp($price,0,$this->scale) ==0) {
            // 无需请求接口【直接返回true】
            $result = [
                'status'    => 'ok',
                'data'  => $tradeDetails['id'],
            ];
        } else {
            // 接口请求参数
            $udpParams = [
                'method'    => static::METHOD_PLACE_ORDER,
                'params'    => [
                    $userKey['account_id'],
                    $amount,
                    $price,
                    $symbol,
                    $apiSellType,
                    $tradeDetails['client_order_id'],
                ],
                'userKey'   => $userKey,
            ];

            // 判断是否需要同步执行
            if($syncOrder = trim(env(self::ENV_KEY_LIST['SYNC_REQUEST_ORDER']))) {
                // 获取同步执行的时间
                $syncTimeout = env(self::ENV_KEY_LIST['SYNC_REQUEST_TIMEOUT']);
                $syncTimeout = empty($syncTimeout)?1:$syncTimeout;
                $syncParams = [];

                list($syncParams['url'],$syncParams['params']) = $this->call($udpParams['method'],array_merge($udpParams['params'],[false]),$udpParams['userKey']);
                // UDP 请求处理
                UdpServer::request(UdpServer::CMD_LIST['CALL_API'],$syncOrder,$syncParams,$syncTimeout);
            }
            $result = $this->call($udpParams['method'],$udpParams['params'],$udpParams['userKey']);
            /******************** 判断为客户订单ID重复的，直接重新获取客户ID，获取不到直接设置网络错误 **********************/
            if($result['status'] == 'error' && static::getErrorType($result['err-code'],$result['err-msg']) == 'CLIENT_ID_ERROR') { //自定义ID错误处理
                for($i=1;$i<=5;++$i) {
                    // 获取记录
                    $result = $this->call(static::METHOD_GET_CLIENT_ORDER,[$udpParams['params'][5]],$userKey);
                    if($result['status'] == 'ok') {
                        $result['data'] = $result['data']['id'];
                        break;
                    } else {
                        $result = [
                            'status' => 'error',
                            'err-code' => -1,
                            'err-msg' => $result['err-msg'],
                        ];
                    }
                    usleep(150000 * $i);
                }

            }
            /****************** END 判断为客户订单ID重复的，直接重新获取客户ID，获取不到直接设置网络错误 ********************/

        }


        if($isSell && ($result['status'] == 'ok' ||  static::getErrorType($result['err-code'],$result['err-msg']) == 'NETWORK' )) {
            // 请求成功，或错误类型是网络状态（非请求失败）时，修改数据为操作卖出成功
            if(!(new CoinRuleRun())->closeMonitor($tradeDetails['coin_rule_run_id'],$tradeDetails['coin_rule_id'],$userKey['version'],[
                'remaining_quantity'  => bcsub($tradeDetails['quantity'],$tradeDetails['sell_api_quantity'],$this->scale),
                'reserved_quantity'  => $tradeDetails['reserved_quantity'],
                'reserved_price'  => $tradeDetails['reserved_price'],
            ])) {
                return false;
            }

        }

        if($result['status'] == 'error') {
            $this->apiCallResult($tradeDetails['id'],static::REQUEST_STATUS_FAIL,[
                static::getErrorType($result['err-code'],$result['err-msg']) == 'NETWORK'?true:false,
                $this->getSaveError($result)
            ],null,true);
            return $returnErrType?static::getErrorType($result['err-code'],$result['err-msg']):false;
        }
        // 调用成功，处理
        if(!$this->apiCallResult($tradeDetails['id'],static::REQUEST_STATUS_SUCCESS,'',$result['data'])) {
            return $returnErrType?'':false;
        }
        return $tradeDetails;
    }


    /**
     * 获取订单详情（通过订单ID）
     * @param $orderId 第三方订单号ID
     * @param array $userKey  用户密钥信息 必须存在字段：account_id、access_key、secret_key
     * @param $isUpdate 判断是否更新数据
     * @param $getDeductFees 判断是否获取抵扣数
     * @return bool|mixed|string
     */
    public function getOrder($orderId,array $userKey,&$isUpdate,$getDeductFees = false) {
        $result = $this->call(static::METHOD_GET_ORDER,[$orderId],$userKey);
        if($result['status'] == 'error') {
            //对不同的错误类型做不同的处理
            switch(static::getErrorType($result['err-code'],$result['err-msg'])) {
                case 'REOCRD_INVALID':  //记录不存在
                    if($this->recordUpdate([
                        ['order_id','=',$orderId],
                        ['status','<>',static::STATUS_FAIL],
                    ],[
                //        'status'    => Db::raw('if(create_time<' . (time() - 60) . ',' . static::STATUS_FAIL . ',status)') ,
                        'error_msg' => $this->getSaveError($result),
                    ])) {
                        $isUpdate = true;
                    }
                    break;

            }
            sleep(1);
            return false;
        }
        $details = $result['data'];
        // 获取抵扣的手续费【null 表示还未能完全获取到】
        if($getDeductFees) {
            $details['deduct_fees'] = $this->getDeductFees($orderId,$userKey,$details['field-amount'],$details['field-fees']);
        }
        return $details;
    }



    /**
     * 通过自定义订单ID获取第三方订单ID
     * @param $clientOrderId    自定义订单ID
     * @param array $userKey  用户密钥信息 必须存在字段：account_id、access_key、secret_key
     * @param $isUpdate 判断是否更新数据
     * @param $getDeductFees 判断是否获取抵扣数
     */
    public function getClientOrder($clientOrderId,array $userKey,&$isUpdate,$getDeductFees = false) {
        // 获取记录
        $result = $this->call(static::METHOD_GET_CLIENT_ORDER,[$clientOrderId],$userKey);

        if($result['status'] == 'error') {
            //对不同的错误类型做不同的处理
            switch(static::getErrorType($result['err-code'])) {
                case 'REOCRD_INVALID':  //记录不存在
                    if($this->recordUpdate([
                        ['client_order_id','=',$clientOrderId],
                        ['status','<>',static::STATUS_FAIL],
                    ],[
                        'status'    => Db::raw('if(create_time<' . (time() - 3600) . ',' . static::STATUS_FAIL . ',status)') ,
                        'error_msg' => $this->getSaveError($result),
                    ])) {
                        $isUpdate = true;
                    }
                    break;

            }
            sleep(1);
            return false;
        }
        $details = $result['data'];
        // 请求获取到数据处理
        if($this->recordUpdate([
            ['client_order_id','=',$clientOrderId],
            ['order_id','=',0],
        ],[
            'order_id'    => $details['id'],
        ])) {
            $isUpdate = true;
        }
        // 通过订单ID获取数据
        return $this->getOrder($details['id'],$userKey,$isUpdate,$getDeductFees);

    }


    /**
     * 获取抵扣的数量
     * @param $orderId  订单ID
     * @param array $userKey    用户账号信息
     * @param $filledAmount  交易数量
     * @param $filledFees 订单中原需扣除的手续费
     */
    private function getDeductFees($orderId,array $userKey,$fieldAmount,$fieldFees) {
        try {
            $result = $this->call(static::METHOD_GET_ORDER_MATCHRESULTS,[$orderId],$userKey);
            if($result['status'] != 'ok') {
                return null;
            }
            $filledAmount = 0;
            $filledFees = 0;

            foreach($result['data'] as $details) {
                $filledAmount = bcadd($filledAmount,$details['filled-amount'],$this->scale);
                $filledFees = bcadd($filledFees,$details['filled-fees'],$this->scale);
            }
            if(bccomp($filledAmount,$fieldAmount,$this->scale) != 0) {
                return null;
            }
        }catch (\Exception $e) {
            return null;
        }

        // 获取抵扣的数量
        return bcsub($fieldFees,$filledFees,$this->scale);
    }


    /**
     * 获取固定默认状态
     */
    public function getDefaultOrder() {
        return [
            'id'            => 0,
            'symbol'        => '',
            'account-id'    => 0,
            'amount'        => 0,
            'price'         => 0,
            'created-at'    => floor(microtime(true) * 1000),
            'type'          => '',
            'field-amount'  => 0,
            'field-cash-amount' => 0,
            'field-fees'    => 0,
            'finished-at'   => time() * 1000,
            'user-id'       => 0,
            'source'    => 'api',
            'state'    =>  'filled', // 订单状态为完成
            'canceled-at'   => floor(microtime(true) * 1000),
        ];
    }


    /**
     * 对比需要修改的信息
     * @param $tradeDetails 本地订单信息
     * @param $orderDetails 第三方订单信息
     */
    public function compTradeUpdateData($tradeDetails,$orderDetails) {
        $newData = [
            'field_amount'  => bcadd($orderDetails['field-cash-amount'],0,$this->scale),
            'field_quantity'    => bcadd($orderDetails['field-amount'],0,$this->scale),
            'field_fees'    => bcadd($orderDetails['field-fees'],0,$this->scale),
            'field_price'   => bcadd(0,0,$this->scale),
            'field_time'    => $orderDetails['finished-at'],
            'request_status'    => static::REQUEST_STATUS_SUCCESS,
        ];

        switch($this->getOrderState($orderDetails['state'])) {
            case 'CREATE':
                $newData['status']  = static::STATUS_APPLY;
                break;
            case 'MATCH':
                $newData['status']  = static::STATUS_MATCH;
                break;
            case 'PARTIAL_FILLED':
                $newData['status']  = static::STATUS_PARTIAL_FILLED;
                break;
            case 'CANCELING':
                $newData['status']  = static::STATUS_CANCELING;
                break;
            case 'FILLED':
                $newData['status']  = static::STATUS_FILLED;
                break;
            case 'PARTIAL_CANCELED':
                $newData['status']  = static::STATUS_PARTIAL_CANCELED;
                break;
            case 'CANCELED':
                $newData['status']  = static::STATUS_CANCELED;
                break;
        }


        if(bccomp($newData['field_quantity'],0,$this->scale) != 0) {
            $newData['field_price'] = bcdiv($newData['field_amount'],$newData['field_quantity'],$this->scale);
        }
        $updateData = [];
        foreach($newData as $key=>$value) {
            if(bccomp($tradeDetails[$key],$value,$this->scale) != 0) {
                $updateData[$key] = $value;
            }
        }
        return $updateData;
    }


    /**
     * 调用 API 接口
     * @param $method 请求的方法
     * @param $params   参数
     * @param $userKey  用户账号信息
     * @return array|mixed|string[] status：ok 请求成功，并操作成功；error：err-code=> -1则为网络错误，第三方情况未知道，其他为第三方请求成功，但操作失败
     */
    public function call($method,$params,$userKey) {
        $apiClass = new Huobi($userKey['account_id'],$userKey['access_key'],$userKey['secret_key']);
        $return = [
            'status'    => 'ok',
            'code'  => 0,
            'msg'   => '',
            'data'  => null,
        ];
        try {
            if(env(static::ENV_CHECK_TEST_KEY)) {
                if(array_key_exists(6,$params) && !$params[6]) {
                    $result = ['https://api.huobi.pro',$params];
                } else {
                    $result = $this->testCall($method,$params);
                }
            } else {
                $result = call_user_func_array([$apiClass,$method],$params);
                $result = json_decode(json_encode($result),true);
            }
            if(empty($result)) {
                throw new \Exception('network error');
            }
        } catch (\Exception $e) {
            $result = [
                'status' => 'error',
                'err-code' => -1,
                'err-msg' => $e->getMessage(),
            ];
        }
        return $result;
    }


    /**
     * 获取测试时的返回信息
     * @param $method
     * @param $params
     */
    public function testCall($method,$params) {
        $result = [
            'status'    => 'ok',
        ];
        switch($method) {
            case static::METHOD_PLACE_ORDER:    //下单的模拟数据
                $find = $this->alias('trade')->where([
                    'client_order_id'   => $params[5]
                ])->field(['id'])->find()->toArray();
                $result['data'] = strval(time()) . strval($find['id']);
                break;
            case static::METHOD_GET_ORDER:  //通过订单ID获取订单详情
                $where = [
                    'order_id'   => $params[0]
                ];
            case static::METHOD_GET_CLIENT_ORDER:   // 通过自编ID获取详情
                if(!isset($where)) {    //查询条件
                    $where = [
                        'client_order_id'   => $params[0]
                    ];
                }
                // 获取记录信息
                $record = $this->alias('trade')
                    ->join('coin coin','coin.id=trade.coin_id','INNER')
                    ->join('users_key key','key.id=trade.user_id','INNER')
                    ->where($where)->field([
                        'trade.*',
                        'coin.code',
                        'key.account_id',
                    ])->find();
                if(!$record) {

                    $result = [
                        'status'    => 'error',
                        'err-code'  => 'base-record-invalid',
                        'err-msg'   => 'record invalid',
                    ];
                } else {
                    $record = $record->toArray();
                    //存在记录处理
                    $result['data'] = [
                        'id'            => $record['order_id']?$record['order_id']:$record['id'],
                        'symbol'        => $record['code'],
                        'account-id'    => $record['account_id'],
                        'amount'        => $record['amount'],
                        'price'         => $record['unit_price'],
                        'created-at'    => $record['create_time'] * 1000,
                        'type'          => $this->getPlaceOrderType($record['type'],null,$record['unit_price']),
                        'field-amount'  => bcsub($record['quantity'],0,rand(1,6)),
                        'field-cash-amount' => bcsub($record['amount'],0,rand(1,6)),
                        'field-fees'    => $record['field_fees'],
                        'finished-at'   => time() * 1000,
                        'user-id'       => $record['user_id'],
                        'source'    => 'api',
                        'state'    => 'filled', // 订单状态
                        'canceled-at'   => 0,
                    ];
                    if($record['type'] == CoinRuleRunTrade::TYPE_BUY) {
                        if(!($details = (new Huobi(1,'',''))->get_market_trade($record['code'])) || $details['status'] != 'ok' ||
                        !array_key_exists('tick',$details) || !array_key_exists('data',$details['tick']) || !array_key_exists(0,$details['tick']['data'])) {
                            $price = 1;
                        } else {
                            $price = $details['tick']['data'][0]['price'];
                        }
                        if(env('call_api.is_input_price')) {    // 获取测试输入的价钱【最后刷入价格】
                            if(($history = HistoryTrade::tcpTableQuery(HistoryTrade::TABLE_INPUT_PRICE . ' market_' . $record['code'] . ' price')) && array_key_exists('result',$history)) {
                                $price = $history['result']?$history['result']:$price;
                            }
                        }
                        $result['data']['field-amount'] = bcdiv($result['data']['field-cash-amount'],$price,$this->scale);
                    } else {
                        $result['data']['field-cash-amount'] = bcmul($result['data']['field-amount'],$record['unit_price'],$this->scale);
                    }

                    if($this->getPlaceOrderType($record['type']) == 'buy-market') {
                        $result['data']['field-fees'] = bcmul($result['data']['field-amount'],0.0002,$this->scale);
                    } else {
                        $result['data']['field-fees'] = bcmul($result['data']['field-cash-amount'],0.0002,$this->scale);
                    }

                }
                break;
        }
        return $result;
    }



    /**
     * 获取订单状态
     * @param $state 原状态
     * @return string
     */
    private function getOrderState($state) {
        $stateList = static::CALL_ORDER_STATE_LIST;
        return array_key_exists($state,$stateList)?$stateList[$state]:$stateList['DEFAULT'];
    }

    /**
     * 获取需要报错的错误数据
     * @param $result
     */
    static public function getSaveError($result) {
        return implode('|',[static::getErrorType($result['err-code'],$result['err-msg']),'code:' . $result['err-code'],'msg:' . $result['err-msg']]);
    }


    /**
     * 获取错误类型
     * @param $errorCode 错误代码
     * @param null $errorMsg 错误信息
     * @return string
     */
    static public function getErrorType($errorCode,$errorMsg = null) {

        $errorList = static::CALL_ERROR_CODE_TYPES;
        $errorType = $errorList['DEFAULT'];
        foreach($errorList as $value=>$errType) {
            $value = explode('|',$value);
            if($value[0] != $errorCode) {
                continue;
            }
            if(!array_key_exists(1,$value) || (is_string($errorMsg) && preg_match($value[1],$errorMsg))) {
                $errorType = $errType;
                break;
            }
        }
        return $errorType;
    }



}
